package com.wa.nashorn;

import java.io.InputStream;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.script.ScriptEngine;

import cn.hutool.core.util.StrUtil;
import jdk.nashorn.api.scripting.ClassFilter;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service;

@Service
public class NashornEngineServiceImpl implements NashornEngineService, InitializingBean {
    private static final Logger LOGGER = LoggerFactory.getLogger(NashornEngineServiceImpl.class);
    private static final int N = 10;
    private ThreadLocal<NashornCompliedScriptHolder> scriptHolderThreadLocal;
    private ThreadLocal<StringBuilder> loggerOutputThreadLocal = new ThreadLocal();
    private ExecutorService rawExecutorService;
    private BlockingQueue<NashornScript<?>> queue;
    private AtomicBoolean started = new AtomicBoolean(false);
    private ThreadFactory threadFactory;
    private Thread[] threads;
    private volatile boolean shouldContinue;
    @Value("${nashorn.class.includes:}")
    private String includeClass;

    public NashornEngineServiceImpl() {
    }

    public void afterPropertiesSet() {
        this.threadFactory = new CustomizableThreadFactory("nashorn-engine");
        this.queue = new LinkedBlockingQueue();
        MyClassFilter classFilter = this.createClassFilter();
        NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
        this.threads = new Thread[10];
        this.scriptHolderThreadLocal = ThreadLocal.withInitial(() -> {
            try {
                ScriptEngine engine = factory.getScriptEngine(classFilter);
                engine.put("nashornObjs", this);
                engine.put("jThreadId", String.valueOf(Thread.currentThread().getId()));
                InputStream in = this.getClass().getResourceAsStream("/javascript/jvm-npm.js");
                engine.eval((new Scanner(in)).useDelimiter("\\A").next());
                in = this.getClass().getResourceAsStream("/javascript/apiService.js");
                engine.eval((new Scanner(in)).useDelimiter("\\A").next());
                NashornCompliedScriptHolder scriptHolder = new NashornCompliedScriptHolder();
                scriptHolder.setScriptEngine(engine);
                return scriptHolder;
            } catch (Exception var6) {
                throw new RuntimeException("Init js engine thread error", var6);
            }
        });
        this.rawExecutorService = Executors.newFixedThreadPool(10, new CustomizableThreadFactory("nashorn-engine-raw-"));
        this.start();
    }

    private MyClassFilter createClassFilter() {
        List<String> includeClassRegex = null;
        if (this.includeClass != null && !this.includeClass.isEmpty()) {
            includeClassRegex = Arrays.asList(this.includeClass.split(","));
        } else {
            includeClassRegex = new LinkedList<>();
            includeClassRegex.add("java.lang.*");
            includeClassRegex.add("java.util.*");
            includeClassRegex.add("java.io.*");
            includeClassRegex.add("com.wa.nashorn.RestCallTemplate");
            includeClassRegex.add("com.wa.nashorn.NashornApiService");
        }

        List<Pattern> patterns = includeClassRegex.stream().map((r) -> Pattern.compile(r.trim())).collect(Collectors.toList());
        return new MyClassFilter(patterns);
    }

    public String getResourceAsStream(String fileName) {
        InputStream in = this.getClass().getResourceAsStream("/javascript/" + fileName);
        if (in == null) {
            throw new RuntimeException("Resource \"" + fileName + "\" missing");
        } else {
            return (new Scanner(in)).useDelimiter("\\A").next();
        }
    }

    public String getResource(String fileName) {
        InputStream in = this.getClass().getResourceAsStream("/javascript/" + fileName);
        if (in == null) {
            throw new RuntimeException("Resource \"" + fileName + "\" missing");
        } else {
            return (new Scanner(in)).useDelimiter("\\A").next();
        }
    }

    public void start() {
        if (!this.started.getAndSet(true)) {
            for(int i = 0; i < 10; ++i) {
                this.start(i);
            }

        }
    }

    private void start(final int i) {
        this.shouldContinue = true;
        this.threads[i] = this.threadFactory.newThread(() -> {
            while(this.shouldContinue) {
                try {
                    this.process((NashornScript)this.queue.take());
                } catch (InterruptedException var2) {
                }
            }

        });
        this.threads[i].start();
    }

    private <O> void process(NashornScript<O> func) {
        Future<O> future = null;

        Object e;
        try {
            future = (Future<O>) this.rawExecutorService.submit(() -> {
                Object var2 = null;
                try {
                    if (func.getLoggerOutputBuilder() != null) {
                        this.loggerOutputThreadLocal.set(func.getLoggerOutputBuilder());
                    }

                    var2 = func.process(this);
                } finally {
                    this.loggerOutputThreadLocal.remove();
                }
                return var2;
            });
            e = future.get(300L, TimeUnit.SECONDS);
            func.onComplete((O) e);
        } catch (Exception var7) {
            e = var7;
            if (var7 instanceof TimeoutException && var7.getMessage() == null) {
                e = new RuntimeException("future timeout exception", var7);
            }

            try {
                this.cancelFuture(future);
                func.onError((Exception)e);
                String scriptStr = func.getScript().replaceAll("\n+|\\s+", "");
                String script = scriptStr.length() > 100 ? scriptStr.substring(0, 100) + " ..." : scriptStr;
                LOGGER.error("Call js function error, script=" + script, (Throwable)e);
            } catch (Exception var6) {
                LOGGER.error("nashorn catch error", var6);
            }
        }

    }

    private void cancelFuture(Future<?> future) {
        if (future != null && !future.isCancelled() && !future.isDone()) {
            future.cancel(true);
        }

    }

    public void push(NashornScript<?> obj) {
        if (!this.started.get()) {
            throw new IllegalStateException("service hasn't be started or was closed");
        } else {
            this.queue.add(obj);
        }
    }

    public void log(String level, String... args) {
        StringBuilder builder = (StringBuilder)this.loggerOutputThreadLocal.get();
        if (builder != null) {
            builder.append(level).append(" >> ").append(StrUtil.join(" ", args)).append("\r\n");
        }

        switch (level) {
            case "info":
                if (LOGGER.isInfoEnabled()) {
                    LOGGER.info("JS LOG >> " + StrUtil.join(" ", args));
                }
                break;
            case "debug":
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("JS LOG >> " + StrUtil.join(" ", args));
                }
                break;
            case "error":
                if (LOGGER.isErrorEnabled()) {
                    LOGGER.error("JS LOG >> " + StrUtil.join(" ", args));
                }
                break;
            case "warn":
                if (LOGGER.isWarnEnabled()) {
                    LOGGER.warn("JS LOG >> " + StrUtil.join(" ", args));
                }
        }

    }

    public void stop() {
        this.started.set(false);
        this.shouldContinue = false;

        for(int i = 0; i < 10; ++i) {
            this.threads[i].interrupt();
        }

    }

    public ScriptObjectMirror getScriptObjectMirror(String key, NashornScript<?> script) {
        NashornCompliedScriptHolder scriptHolder = (NashornCompliedScriptHolder)this.scriptHolderThreadLocal.get();
        return scriptHolder.compile(key, script.getScript());
    }

    private static class MyClassFilter implements ClassFilter {
        private List<Pattern> patterns;

        MyClassFilter(List<Pattern> patterns) {
            this.patterns = patterns;
        }

        public boolean exposeToScripts(String clzName) {
            Iterator var2 = this.patterns.iterator();

            Pattern pattern;
            do {
                if (!var2.hasNext()) {
                    return false;
                }

                pattern = (Pattern)var2.next();
            } while(!pattern.matcher(clzName).matches());

            return true;
        }
    }
}
